Ziel dieses letzten Notebooks ist es die zuvor gescrapten Daten zu kombinieren und auszuwerten. Dafür wird das bestmögliche Regressionsmodel zur Vorhersage des Kaufpreises von Häusern bestimmt. Dazu wird zuvor eine explorative Datenanalyse (EDA) durchgeführt, um Strukturen und Besonderheiten in den Daten zu erkennen und hervorzuheben. Danach werden die Daten für die Regression aufbereitet. Im Anschluss werden verschiedene Regressionsmodelle erstellt, optimiert und miteinander verglichen.
Ein weiterer Teil dieses Notebooks ist es die Bauzinsen einzubeziehen. Da wir nur einen kurzen Zeitraum betrachtet haben, in dem sich die Zinsen nicht stark verändert haben, ergibt es keinen Sinn, diese als Variable in die Regressionsanalyse aufzunehmen. Es wird trotzallem die Entwicklung des Bauzinses mit den Kaufpreisen über die Zeit betrachtet und auf mögliche Trends und Zusammenhänge untersucht.
Als Voraussetzung zur Funktionstüchtigkeit des Codes, werden im ersten Schritt die benötigten Libraries erstellt. Diese werden für folgende Befehle benötigt:
import pandas as pd
import numpy as np
import seaborn as sns
import plotly.express as px
import plotly.graph_objs as go
import plotly.figure_factory as ff
from scipy import stats
import statsmodels.formula.api as smf
from statsmodels.formula.api import ols
import statsmodels.api as sm
from time import time
from patsy import dmatrices
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn import svm
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LinearRegression, BayesianRidge, SGDRegressor, LassoCV
from sklearn.svm import SVR
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.metrics import mean_absolute_error, median_absolute_error
from sklearn.pipeline import Pipeline
import matplotlib.pyplot as plt
In diesem Kapitel werden die Häuserdaten analysiert und auf Strukturen, Besonderheiten und Zusammenhänge untersucht.
Als erstes wird die täglichen Immobililien csv-Dateien eingelesen, verknüpft und auf deren Korrektheit überprüft.
immo2812=pd.read_csv("daily_data//immo//2022-12-28_Immobilien.csv", engine='python')
immo2912=pd.read_csv("daily_data//immo//2022-12-29_Immobilien.csv", engine='python')
immo3012=pd.read_csv("daily_data//immo//2022-12-30_Immobilien.csv", engine='python')
immo3112=pd.read_csv("daily_data//immo//2022-12-31_Immobilien.csv", engine='python')
immo0101=pd.read_csv("daily_data//immo//2023-01-01_Immobilien.csv", engine='python')
immo0201=pd.read_csv("daily_data//immo//2023-01-02_Immobilien.csv", engine='python')
immo0301=pd.read_csv("daily_data//immo//2023-01-03_Immobilien.csv", engine='python')
immo0401=pd.read_csv("daily_data//immo//2023-01-04_Immobilien.csv", engine='python')
immo2812.head()
| Unnamed: 0 | ID | Ort | Umkreis | MaxOrt | Preis | Fläche | Zimmer | Grundstücksfläche | Kategorie | Etagen | Baujahr | Effizienzklasse | Energieträger | Heizungsart | Stand | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 28zn659 | berlin | 50 | max. 15 km | 7.790.000 € | ['780 m²'] | ['30 Zi.'] | ['1.696 m²'] | ['Villa'] | ['5 Geschosse'] | ['1890'] | n.a | n.a | n.a | 2022-12-28 |
| 1 | 1 | 28jr359 | berlin | 50 | max. 30 km | 450.000 € | ['111 m²'] | ['5 Zi.'] | ['1.100 m²'] | ['Einfamilienhaus'] | ['2 Geschosse'] | ['1936'] | ['H'] | ['Gas, Kohle'] | ['Zentralheizung'] | 2022-12-28 |
| 2 | 2 | 274he5l | berlin | 50 | max. 10 km | 690.000 € | ['323 m²'] | ['8 Zi.'] | ['393 m²'] | ['Mehrfamilienhaus'] | n.a | ['1990'] | ['D'] | ['Gas'] | ['Zentralheizung'] | 2022-12-28 |
| 3 | 3 | 26zkl56 | berlin | 50 | max. 10 km | 429.000 € | ['97 m²'] | ['4 Zi.'] | ['407 m²'] | ['Einfamilienhaus'] | n.a | ['1958'] | ['E'] | ['Öl'] | ['Zentralheizung'] | 2022-12-28 |
| 4 | 4 | 279kt5y | berlin | 50 | max. 10 km | 1.700.000 € | ['186.62 m²'] | ['8 Zi.'] | ['502 m²'] | ['Mehrfamilienhaus'] | n.a | ['1936'] | n.a | ['Fernwärme'] | ['Zentralheizung'] | 2022-12-28 |
immodf = pd.concat([immo2812,immo2912,immo3012,immo3112,immo0101,immo0201,immo0301,immo0401],ignore_index=True)
print(immodf.shape)
immodf.head()
(55060, 16)
| Unnamed: 0 | ID | Ort | Umkreis | MaxOrt | Preis | Fläche | Zimmer | Grundstücksfläche | Kategorie | Etagen | Baujahr | Effizienzklasse | Energieträger | Heizungsart | Stand | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 28zn659 | berlin | 50 | max. 15 km | 7.790.000 € | ['780 m²'] | ['30 Zi.'] | ['1.696 m²'] | ['Villa'] | ['5 Geschosse'] | ['1890'] | n.a | n.a | n.a | 2022-12-28 |
| 1 | 1 | 28jr359 | berlin | 50 | max. 30 km | 450.000 € | ['111 m²'] | ['5 Zi.'] | ['1.100 m²'] | ['Einfamilienhaus'] | ['2 Geschosse'] | ['1936'] | ['H'] | ['Gas, Kohle'] | ['Zentralheizung'] | 2022-12-28 |
| 2 | 2 | 274he5l | berlin | 50 | max. 10 km | 690.000 € | ['323 m²'] | ['8 Zi.'] | ['393 m²'] | ['Mehrfamilienhaus'] | n.a | ['1990'] | ['D'] | ['Gas'] | ['Zentralheizung'] | 2022-12-28 |
| 3 | 3 | 26zkl56 | berlin | 50 | max. 10 km | 429.000 € | ['97 m²'] | ['4 Zi.'] | ['407 m²'] | ['Einfamilienhaus'] | n.a | ['1958'] | ['E'] | ['Öl'] | ['Zentralheizung'] | 2022-12-28 |
| 4 | 4 | 279kt5y | berlin | 50 | max. 10 km | 1.700.000 € | ['186.62 m²'] | ['8 Zi.'] | ['502 m²'] | ['Mehrfamilienhaus'] | n.a | ['1936'] | n.a | ['Fernwärme'] | ['Zentralheizung'] | 2022-12-28 |
Das entstandene Dataframe hat 55060 Zeilen und 16 Spalten.
Bei der Betrachtung des heads von Dataframe immodf fällt auf, dass viele nicht benötigten Sonderzeichen und Buchstaben vohanden sind. Diese werden im nächsten Schritt entfernt und wenn reine Zahlenwerte übrig bleiben, werden diese auch in einen numerischen Datentyp umgewandelt.
#MaxOrt
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace("max. ","")
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace(" km","")
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace(",",".")
immodf.loc[:,"MaxOrt"]=pd.to_numeric(immodf["MaxOrt"]) #in numerischen Datentyp umwandeln
#Preis
immodf["Preis"]=immodf["Preis"].str.replace(".","")
immodf["Preis"]=immodf["Preis"].str.replace(" €","")
immodf["Preis"]=immodf["Preis"].str.replace("auf Anfrage","0")
immodf["Preis"] = immodf["Preis"].apply(lambda x: x.split(',')[0]) #Ganzzahlen reichen aus
immodf.loc[:,"Preis"]=pd.to_numeric(immodf["Preis"])
#Fläche
immodf['Flaeche'] = immodf['Fläche'].apply(lambda x: x[1:-1]) #neue Spalte generieren, dass Listenmodul aufgelöst wird
immodf['Flaeche'] = immodf['Flaeche'].str.replace(" m²","")
immodf['Flaeche'] = immodf['Flaeche'].str.replace(".","")
immodf['Flaeche'] = immodf['Flaeche'].str.replace("'","")
immodf.loc[:,"Flaeche"]=pd.to_numeric(immodf["Flaeche"])
immodf = immodf.drop('Fläche', axis=1) #alte Spalte löschen
#Zimmer
immodf['Raeume'] = immodf['Zimmer'].apply(lambda x: x[1:-1])
immodf['Raeume'] = immodf['Raeume'].str.replace(" Zi","")
immodf['Raeume'] = immodf['Raeume'].str.replace(".","")
immodf['Raeume'] = immodf['Raeume'].str.replace("'","")
immodf.loc[:,'Raeume']=pd.to_numeric(immodf['Raeume'])
immodf = immodf.drop('Zimmer', axis=1)
#Grundstücksfläche
immodf['Grundstuecksflaeche'] = immodf['Grundstücksfläche'].apply(lambda x: x[1:-1])
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(" m²","")
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(".","")
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace("'","")
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(",",".")
immodf['Grundstuecksflaeche']=immodf['Grundstuecksflaeche'].str.replace("kA","0")
immodf.loc[:,"Grundstuecksflaeche"]=pd.to_numeric(immodf["Grundstuecksflaeche"])
immodf = immodf.drop('Grundstücksfläche', axis=1)
#Kategorie
immodf['Art'] = immodf['Kategorie'].apply(lambda x: x[1:-1])
immodf['Art'] = immodf['Art'].str.replace("'","")
immodf['Art'] = immodf['Art'].str.replace(".","")
immodf = immodf.drop('Kategorie', axis=1)
#Etagen
immodf['Geschosse'] = immodf['Etagen'].apply(lambda x: x[1:-1])
immodf['Geschosse'] = immodf['Geschosse'].str.replace(" Geschosse","")
immodf['Geschosse'] = immodf['Geschosse'].str.replace(" Geschoss","")
immodf['Geschosse'] = immodf['Geschosse'].str.replace("'","")
immodf['Geschosse'] = immodf['Geschosse'].str.replace("n.a","0")
immodf['Geschosse'] = immodf['Geschosse'].str.replace(".","")
immodf.loc[:,"Geschosse"]=pd.to_numeric(immodf["Geschosse"])
immodf = immodf.drop('Etagen', axis=1)
#Baujahr
immodf['Jahr'] = immodf['Baujahr'].apply(lambda x: x[1:-1])
immodf['Jahr'] = immodf['Jahr'].str.replace("'","")
immodf['Jahr'] = immodf['Jahr'].str.replace("(","")
immodf['Jahr'] = immodf['Jahr'].str.replace("n.a","0")
immodf['Jahr'] = immodf['Jahr'].str.replace(".","")
immodf['Jahr'] = immodf['Jahr'].str.replace("ca ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("ca","")
immodf['Jahr'] = immodf['Jahr'].str.replace("um ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("saniert ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("3 Quartal ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("Baubeginn ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("Neubau","2023")
immodf['Jahr'] = immodf['Jahr'].str.replace("renoviert ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("vor ","")
immodf['Jahr'] = immodf['Jahr'].str.replace(" 1997 umfa","")
immodf['Jahr'] = immodf['Jahr'].str.replace("unbekannt ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("nicht bek ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("nicht bekannt","")
immodf['Jahr'] = immodf['Jahr'].str.replace(" 2023","")
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split('/')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(' -')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split('-')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(',')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(' und')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(' +')[0])
immodf.loc[:,"Jahr"]=pd.to_numeric(immodf["Jahr"])
immodf = immodf.drop('Baujahr', axis=1)
#Effizienzklasse
immodf['Effizienz'] = immodf['Effizienzklasse'].apply(lambda x: x[1:-1])
immodf['Effizienz'] = immodf['Effizienz'].str.replace("'","")
immodf['Effizienz'] = immodf['Effizienz'].str.replace(".","")
immodf = immodf.drop('Effizienzklasse', axis=1)
#Energieträger
immodf['Energietraeger'] = immodf['Energieträger'].apply(lambda x: x[1:-1])
immodf['Energietraeger'] = immodf['Energietraeger'].str.replace("'","")
immodf['Energietraeger'] = immodf['Energietraeger'].str.replace(".","")
immodf = immodf.drop('Energieträger', axis=1)
#Heizungsart
immodf['Heizung'] = immodf['Heizungsart'].apply(lambda x: x[1:-1])
immodf['Heizung'] = immodf['Heizung'].str.replace("'","")
immodf['Heizung'] = immodf['Heizung'].str.replace(".","")
immodf = immodf.drop('Heizungsart', axis=1)
#Unnütze Variablen entfernen
immodf = immodf.drop('Unnamed: 0', axis=1) #nur ein Zählwert, schon durch Zeilennummer gegeben
immodf = immodf.drop('Umkreis', axis=1) #immer 50, da ein Umkreis von 50 km um jede Stadt betrachtet wird
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:2: FutureWarning: The default value of regex will change from True to False in a future version.
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace("max. ","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:8: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf["Preis"]=immodf["Preis"].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:17: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Flaeche'] = immodf['Flaeche'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:25: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Raeume'] = immodf['Raeume'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:33: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:43: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Art'] = immodf['Art'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:51: FutureWarning: The default value of regex will change from True to False in a future version.
immodf['Geschosse'] = immodf['Geschosse'].str.replace("n.a","0")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:52: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Geschosse'] = immodf['Geschosse'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:59: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Jahr'] = immodf['Jahr'].str.replace("(","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:60: FutureWarning: The default value of regex will change from True to False in a future version.
immodf['Jahr'] = immodf['Jahr'].str.replace("n.a","0")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:61: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Jahr'] = immodf['Jahr'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:88: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Effizienz'] = immodf['Effizienz'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:94: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Energietraeger'] = immodf['Energietraeger'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:100: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
immodf['Heizung'] = immodf['Heizung'].str.replace(".","")
Zur Überprüfung, ob die Daten nun den erwünschten Format entsprechen, wird der Kopf und die Info des dataframes angezeigt.
immodf.head()
| ID | Ort | MaxOrt | Preis | Stand | Flaeche | Raeume | Grundstuecksflaeche | Art | Geschosse | Jahr | Effizienz | Energietraeger | Heizung | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 28zn659 | berlin | 15.0 | 7790000 | 2022-12-28 | 780.0 | 30.0 | 1696.0 | Villa | 5.0 | 1890.0 | |||
| 1 | 28jr359 | berlin | 30.0 | 450000 | 2022-12-28 | 111.0 | 5.0 | 1100.0 | Einfamilienhaus | 2.0 | 1936.0 | H | Gas, Kohle | Zentralheizung |
| 2 | 274he5l | berlin | 10.0 | 690000 | 2022-12-28 | 323.0 | 8.0 | 393.0 | Mehrfamilienhaus | NaN | 1990.0 | D | Gas | Zentralheizung |
| 3 | 26zkl56 | berlin | 10.0 | 429000 | 2022-12-28 | 97.0 | 4.0 | 407.0 | Einfamilienhaus | NaN | 1958.0 | E | Öl | Zentralheizung |
| 4 | 279kt5y | berlin | 10.0 | 1700000 | 2022-12-28 | 18662.0 | 8.0 | 502.0 | Mehrfamilienhaus | NaN | 1936.0 | Fernwärme | Zentralheizung |
immodf.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 55060 entries, 0 to 55059 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ID 55060 non-null object 1 Ort 55060 non-null object 2 MaxOrt 55060 non-null float64 3 Preis 55060 non-null int64 4 Stand 55060 non-null object 5 Flaeche 54515 non-null float64 6 Raeume 51591 non-null float64 7 Grundstuecksflaeche 54842 non-null float64 8 Art 55060 non-null object 9 Geschosse 9822 non-null float64 10 Jahr 48476 non-null float64 11 Effizienz 55060 non-null object 12 Energietraeger 55060 non-null object 13 Heizung 55060 non-null object dtypes: float64(6), int64(1), object(7) memory usage: 5.9+ MB
Es gibt insgesamt 55060 Zeilen, also zu verkaufende Häuser, die betrachtet werden. Die verwendeten 14 Variablen sind folgende: Column Name | Description | Data Type :------------------------ | :---------------- |:-----------------: ID | Einzigartige Identifikationsnummer des Hauses | Object Ort | Ortsname, in/um den das Haus steht | Object MaxOrt | Maximaler Abstand zum Ort in km | Integer Preis | Hauspreis | Integer Stand | Datum des Scrapings | Object Flaeche | Wohnfläche in m2 | Float Raeume | Anzahl der Zimmer im Haus | Float Grundstuecksflaeche | Grundstücksfläche in m2 | Float Art | Kategorie des Hauses | Object Geschosse | Anzahl der Stockwerke im Haus | Float Jahr | Baujahr des Hauses | Float Effizienz | Effizienzklasse (A++ - H) | Object Energietraeger | Verwendete Stoffe zur Energieerzeugung | Oject Heizung | Art der verbauten Heizung | Object
Aus der Info lässt sich ablesen, dass einige Nullwerte vorhanden sind. Dies wird nun visualisiert für eine einfachere Entscheidung, ob diese Zeilen entfernt werden. Nullwerte im folgenden Graphen in gelb angezeigt. Es werden nur numerische Spalten angezeigt.
sns.heatmap(immodf.isnull(), yticklabels=False, cbar=False, cmap='viridis')
<AxesSubplot:>
Bei Geschosse sind sehr viele Nullwerte vorhanden. Ein Löschen all dieser Zeilen im dataframe, würde dieses zu stark verkleinern, daher werden alle Zeilen mit Nullwerten beibehalten und durch den Mittelwert (bei numerischen Variablen) oder dem nächsten Wert (bei kategorischen Variablen) aller Einträge ersetzt.
Bei der manuellen Sichtung der csv-Dateien, ist in einer aufgefallen, dass am Ende einige Zeilen komplett mit Nullwerten sind. Diese werden ebenfalls entfernt.
#für numerische Spalten
immodf=immodf.fillna(immodf.mean())
#für kategorische Spalten
immodf = immodf[immodf.Ort != "0"]
immodf.loc[immodf.Effizienz == "", 'Effizienz'] = np.nan
immodf.loc[immodf.Energietraeger == "", 'Energietraeger'] = np.nan
immodf.loc[immodf.Heizung == "", 'Heizung'] = np.nan
immodf.loc[immodf.Art == "", 'Art'] = np.nan
immodf = immodf.fillna(method="bfill") #bfill umd durch nächsten Wert zu ersetzen
immodf.head()
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/3599110604.py:2: FutureWarning: Dropping of nuisance columns in DataFrame reductions (with 'numeric_only=None') is deprecated; in a future version this will raise TypeError. Select only valid columns before calling the reduction. immodf=immodf.fillna(immodf.mean())
| ID | Ort | MaxOrt | Preis | Stand | Flaeche | Raeume | Grundstuecksflaeche | Art | Geschosse | Jahr | Effizienz | Energietraeger | Heizung | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 28zn659 | berlin | 15.0 | 7790000 | 2022-12-28 | 780.0 | 30.0 | 1696.0 | Villa | 5.000000 | 1890.0 | H | Gas, Kohle | Zentralheizung |
| 1 | 28jr359 | berlin | 30.0 | 450000 | 2022-12-28 | 111.0 | 5.0 | 1100.0 | Einfamilienhaus | 2.000000 | 1936.0 | H | Gas, Kohle | Zentralheizung |
| 2 | 274he5l | berlin | 10.0 | 690000 | 2022-12-28 | 323.0 | 8.0 | 393.0 | Mehrfamilienhaus | 2.417838 | 1990.0 | D | Gas | Zentralheizung |
| 3 | 26zkl56 | berlin | 10.0 | 429000 | 2022-12-28 | 97.0 | 4.0 | 407.0 | Einfamilienhaus | 2.417838 | 1958.0 | E | Öl | Zentralheizung |
| 4 | 279kt5y | berlin | 10.0 | 1700000 | 2022-12-28 | 18662.0 | 8.0 | 502.0 | Mehrfamilienhaus | 2.417838 | 1936.0 | D | Fernwärme | Zentralheizung |
Wir vermuten, dass viele Häuser über mehrere Tage inseriert sind und somit mehrfach vorkommen. Für die Regression soll jedes Inserat jedoch nur ein Mal gelistet sein, daher werden die Duplikate entfernt. Außerdem wird überprüft, ob Inserate mehrfach unter unterschiedlicher ID vorkommen. Diese identifizierten Duplikate werden ebenfalls entfernt. Es wird ein neues Dataframe df erstellt, indem die Duplikate entfernt sind. Für die Berechnung der Inventardauer der Inserate werden die Duplikate und somit immodf noch benötigt.
for col in immodf.columns:
values = immodf[col].unique()
print(col, "has", len(immodf[col].unique()), "unique values")
ID has 7963 unique values Ort has 7 unique values MaxOrt has 10 unique values Preis has 1533 unique values Stand has 6 unique values Flaeche has 1942 unique values Raeume has 72 unique values Grundstuecksflaeche has 1734 unique values Art has 14 unique values Geschosse has 8 unique values Jahr has 202 unique values Effizienz has 11 unique values Energietraeger has 60 unique values Heizung has 35 unique values
Das Dataframe hat 55060 Zeilen, jedoch nur 7963 einzigartige Werte für ID.
df = immodf.drop_duplicates(subset=["ID"], keep='first')
df = immodf.drop_duplicates(subset=["Ort", "MaxOrt", "Preis", "Flaeche", "Raeume", "Grundstuecksflaeche", "Art", "Energietraeger", "Heizung"], keep='first')
df.shape
(8169, 14)
Nach dem Entfernen von Duplikaten bestehen noch 8169 der ursprünglich 55060 Häuser im Dataframe.
In diesem Kapitel werden die numerischen Features im dataframe untersucht. Zuerst werden diese einzeln auf Ausprägungen und Verteilungen analysiert, danach auch deren Zusammenhänge.
Welche numerischen Features gibt es in df?
numeric_features=df.select_dtypes(include=np.number).columns.to_list()
numeric_features
['MaxOrt', 'Preis', 'Flaeche', 'Raeume', 'Grundstuecksflaeche', 'Geschosse', 'Jahr']
Es gibt 7 numerische Features in df. Deren Ausprägungen werden nun analysiert.
df.describe().applymap('{:,.2f}'.format)
| MaxOrt | Preis | Flaeche | Raeume | Grundstuecksflaeche | Geschosse | Jahr | |
|---|---|---|---|---|---|---|---|
| count | 8,169.00 | 8,169.00 | 8,169.00 | 8,169.00 | 8,169.00 | 8,169.00 | 8,169.00 |
| mean | 20.48 | 1,054,410.19 | 3,410.98 | 11.00 | 733.42 | 2.42 | 1,967.25 |
| std | 13.03 | 1,399,783.44 | 10,733.07 | 17.40 | 3,705.69 | 0.38 | 54.46 |
| min | 0.50 | 0.00 | 1.00 | 1.00 | 0.00 | 1.00 | 23.00 |
| 25% | 10.00 | 495,617.00 | 133.00 | 5.00 | 259.50 | 2.42 | 1,955.00 |
| 50% | 15.00 | 725,000.00 | 180.00 | 6.00 | 460.00 | 2.42 | 1,967.56 |
| 75% | 30.00 | 1,143,000.00 | 519.00 | 9.00 | 735.00 | 2.42 | 1,995.00 |
| max | 50.00 | 30,000,000.00 | 307,673.00 | 415.00 | 195,940.00 | 8.00 | 2,024.00 |
MaxOrt: Der geringste maximale Abstand zum Stadtzentrum sind 0,5 km und der höchste 50 km. Der Median liegt bei 15 km und der Mittelwert bei 20 km. MaxOrt scheint daher recht gleichmäßig verteilt zu sein, wobei die meisten Häuser eher näher am Stadtzentrum liegen.
Preis: Das erste Quartil endet bei knapp 0,5 Millionen € und das Dritte bei knapp über einer Million €. Der Median liegt bei 725.000€ und der Mittelwert bei über einer Million €. Dies weist daraufhin, dass es einige sehr teure Häuser, bis zu 30 Mio. € gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.
Flaeche: Das erste Quartil endet bei 133 m2 und das Dritte bei 519 m2. Der Median liegt bei 180 m2 und der Mittelwert bei knapp 3500 m2. Dies weist daraufhin, dass es einige sehr große Häuser, bis zu über 300.000 m2 gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.
Raume: Das erste Quartil endet bei 5 Zimmern und das Dritte bei 9 Zimmern. Der Median liegt bei 6 Zimmern und der Mittelwert bei 11 Zimmern. Dies weist daraufhin, dass es einige Häuser mit sehr vielen Zimmern bis zu 415 Zimmern gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.
Grundstuecksflaeche: Das erste Quartil endet bei 259 m2 und das Dritte bei 735 m2. Der Median liegt bei 460 m2 und der Mittelwert bei 733 m2. Dies weist daraufhin, dass es einige Häuser mit sehr großen Grundstück bis zu knapp 200.000 m2 gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.
Geschosse: Das erste bis Dritte Quartil, sowie der mittelwert liegen 2,4 Stockwerken. Dies ist auf die große Anzahl an Nullwerten zurückzuführen.
Jahr: Das erste Quartil endet beim Jahr 1955 und das Dritte bei 1995. Der Median und Mittelwert liegen bei 1967. Die Jahre scheinen recht gleichmäßig verteilt zu sein, mit ein paar Ausreißern zum Minimum vom Jahr 23.
Nach diesem Überblick, werden die Lagemaße der Features visualisiert und auf ggf. weitere Erkenntnisse zu den einzelnen Variablen eingegangen.
#Preis
fig = px.histogram(df, x="Preis", nbins=100,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Es handelt sich tatsächlich um einige Ausreißer mit Preisen zwischen 2 und 30 Millionen €.
#MaxOrt
fig = px.histogram(df, x="MaxOrt", nbins=20,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Die meisten Häuser liegen zwischen 8 und 22 km vom Stadtzentrum entfernt.
#Flaeche
fig = px.histogram(df, x="Flaeche", nbins=100,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Es handelt sich bereits um Ausreißer ab einer Wohnfläche von 1100 m2. Es gibt jedoch einige Ausreißer.
#Raeume
fig = px.histogram(df, x="Raeume", nbins=100,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Es handelt sich bereits um Ausreißer ab 15 Räumen je Haus. Es gibt jedoch einige Ausreißer.
#Grundstuecksflaeche
fig = px.histogram(df, x="Grundstuecksflaeche", nbins=100,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Es handelt sich bereits um Ausreißer ab einer Grundstücksfläche von 1447 m2. Es gibt jedoch einige Ausreißer.
#Geschosse
fig = px.histogram(df, x="Geschosse", nbins=8,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Mit Abstand die meisten Häuser haben 2 Stockwerke, welches den Median und Mittelwert von 2,4 begründet.
#Jahr
fig = px.histogram(df, x="Jahr", nbins=100,
marginal="box",
hover_data=["Ort", "ID"])
fig.show()
Es handelt sich um Ausreißer bei Häusern, die vor 1895 gebaut wurden. Die Exposes der extremen Ausreißer wurden betrachtet und die Zeilen werden entfernt.
df = df.query('Jahr > 200')
Die Korrelationsmatrix gibt den linearen Zusammenhang zweier numerischer Features an. Ein Wert von 0 bedeutet dabei keinen linearen Zusammenhang zwischen den Variablen, 1 den stärksten positiven und -1 den stärksten negativen linearen Zusammenhang. Bei Werten von größer als 0,5 oder kleiner als -0,5 wird von einem starkem linearen Zusammenhang gesprochen.
Die Korrelationsmatrix beurteilt ausschließlich lineare Zusammenhänge. Das bedeutet, dass trotz geringem Wert in der Korrelationsmatrix ein (nicht linearer) Zusammenhang bestehen kann, aber nicht muss.
In der gewählten Darstellung werden die Korrelationen sowohl farblich als auch numerisch angegeben. Wichtig für diese Analyse sind die Zusammenhänge mit einer bestimmten Variable, also welche anderen Variablen das gewählte Feature am meisten beinflussen. Es kann daher entweder die Zeile oder Spalte von des gewählten Features angeschaut werden und nach den stärksten Zusammenhängen untersucht werden.
Erwartungen:
corr = df.corr()
sns.heatmap(corr, cmap="Blues", annot=True)
<AxesSubplot:>
Ergebnisse:
Als nächstes werden die Zusammenhänge der numerischen Features noch in einem Scatterplot betrachtet, dass nicht lineare Zusammenhänge visuell herausgefunden werden können.
#Scattermatrix
fig = px.scatter_matrix(df[numeric_features],height=800)
fig.update_traces(dict(opacity=0.3,marker=go.splom.Marker(size=3))
)
fig.show("notebook")
Aus der Scattermatrix lassen sich keine weiteren Zusammenhänge mit Preis ablesen, welche nicht linearer Form sind.
In diesem Kapitel werden wie zuvor die numerischen, nun die kategorischen Features im Dataframe untersucht. Zuerst werden diese einzeln auf Ausprägungen und Verteilungen analysiert, danach auch deren Zusammenhänge.
Welche kategorischen Features gibt es in df?
cat_features=df.select_dtypes(exclude=np.number).columns.to_list()
cat_features
['ID', 'Ort', 'Stand', 'Art', 'Effizienz', 'Energietraeger', 'Heizung']
Es gibt sieben kategorische Features, wovon eines die einzigartige ID ist. Für einen ersten Überblick über die Features, wird ein parellel categories plot verwendet. Dieser zeigt welche Ausprägungen und in welcher Konfiguration vorkommen.
fig = px.parallel_categories(df[cat_features])
fig.show("notebook")
Die sieben Orte sind recht gleichmäßig verteilt und am häufigsten stehen Einfamilienhäuser und Häuser mit einer Zentralheizung als Heizungsart zu Verkauf.
#Wie viele Häuser werden je Stadt betrachtet?
df["Ort"].value_counts()
hamburg 1214 koeln 1195 frankfurt-am-main 1183 berlin 1160 muenchen 1157 stuttgart 1139 leipzig 1118 Name: Ort, dtype: int64
Die Anzahl der Häuser je Stadt ist sehr änhlich, am meisten sind jedoch in Hamburg und am wenigsten in Leipzig.
#Art
df["Art"].value_counts()[:5].plot(kind="bar")
<AxesSubplot:>
Die häufigste Art von Häusern sind Einfamilienhäusern, gefolgt von Mehrfamilienhäusern, sowie Doppelhaushälften und Reihenhäusern.
#Effizienz
df["Effizienz"].value_counts().plot(kind="bar")
<AxesSubplot:>
Die häufigste Effizienzklasse ist H, die zweitschlechteste F, gefolgt von D, E und G. Gute Effizienzklassen hingegen sind deutlich seltener.
#Heizung
df["Heizung"].value_counts()[:5].plot(kind="bar")
<AxesSubplot:>
Mit Abstand die häufigste Heizungsart ist die Zentralheizung. Mit nur rund einem Fünfteltel davon gefolgt sind die Kombination Zentralheizung und offener Kamin, sowie die Fußbodenheizung.
#Energietraeger
df["Energietraeger"].value_counts()[:5].plot(kind="bar")
<AxesSubplot:>
Der häufigste Energieträger ist Gas und am zweithäufigsten, aber nur halb so oft vorkommend ist Öl. Alle anderen Energieträger haben einen sehr kleinen Anteil.
Nun werden die kategorischen Features mit einander in Verbindung gebracht. Zuerst werden conditional probabilities betrachet, bei denen zwei Variablen und deren gemeinsames Auftreten als Häufigkeit in tabellarischer Form angegeben werden.
#Ort und Art
pd.crosstab(df["Ort"],df["Art"], normalize = "columns")
| Art | Bauernhaus | Bungalow | Burg/Schloss | Doppelhaushälfte | Einfamilienhaus | Finca | Herrenhaus | Mehrfamilienhaus | Reihenendhaus | Reihenmittelhaus | Rustico | Stadthaus | Villa | Wohn- und Geschäftshaus |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ort | ||||||||||||||
| berlin | 0.068182 | 0.163180 | 0.000000 | 0.116134 | 0.197913 | 0.0 | 0.4 | 0.097475 | 0.101868 | 0.106667 | 0.0 | 0.052083 | 0.196154 | 0.2 |
| frankfurt-am-main | 0.113636 | 0.129707 | 0.000000 | 0.120306 | 0.130783 | 0.0 | 0.2 | 0.189665 | 0.159593 | 0.112222 | 0.0 | 0.083333 | 0.269231 | 0.1 |
| hamburg | 0.136364 | 0.267782 | 0.000000 | 0.118915 | 0.155826 | 0.0 | 0.2 | 0.099824 | 0.149406 | 0.188889 | 0.0 | 0.437500 | 0.203846 | 0.1 |
| koeln | 0.022727 | 0.150628 | 0.666667 | 0.160640 | 0.134957 | 0.0 | 0.0 | 0.167352 | 0.157895 | 0.157778 | 0.0 | 0.072917 | 0.034615 | 0.1 |
| leipzig | 0.500000 | 0.125523 | 0.333333 | 0.077191 | 0.171130 | 1.0 | 0.0 | 0.165003 | 0.040747 | 0.120000 | 0.0 | 0.135417 | 0.126923 | 0.1 |
| muenchen | 0.090909 | 0.075314 | 0.000000 | 0.251043 | 0.093913 | 0.0 | 0.0 | 0.081033 | 0.256367 | 0.185556 | 1.0 | 0.125000 | 0.130769 | 0.0 |
| stuttgart | 0.068182 | 0.087866 | 0.000000 | 0.155772 | 0.115478 | 0.0 | 0.2 | 0.199648 | 0.134126 | 0.128889 | 0.0 | 0.093750 | 0.038462 | 0.4 |
Die häufigste Hausart Einfamilienhaus ist in allen Städten verfügbar, am häufigsten jedoch in Leipzig und Berlin. 50% der Bauernhäuser liegen rund um Leipzig und 44% der inserierten Stadthäuser sind in Hamburg.
#Art und Effizienz
pd.crosstab(df["Art"],df["Effizienz"], normalize = "columns")
| Effizienz | A | A+ | B | C | D | E | E, H | F | G | H |
|---|---|---|---|---|---|---|---|---|---|---|
| Art | ||||||||||
| Bauernhaus | 0.002174 | 0.000000 | 0.009225 | 0.007075 | 0.005978 | 0.000856 | 0.000 | 0.006240 | 0.004090 | 0.009181 |
| Bungalow | 0.017391 | 0.020050 | 0.016605 | 0.029481 | 0.023911 | 0.026541 | 0.250 | 0.035881 | 0.032720 | 0.038256 |
| Burg/Schloss | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000 | 0.000000 | 0.001022 | 0.001530 |
| Doppelhaushälfte | 0.223913 | 0.270677 | 0.162362 | 0.192217 | 0.175064 | 0.179795 | 0.000 | 0.162246 | 0.159509 | 0.149197 |
| Einfamilienhaus | 0.336957 | 0.325815 | 0.324723 | 0.331368 | 0.343296 | 0.363014 | 0.125 | 0.326833 | 0.380368 | 0.394032 |
| Finca | 0.000000 | 0.000000 | 0.001845 | 0.000000 | 0.000000 | 0.000856 | 0.000 | 0.000000 | 0.000000 | 0.000000 |
| Herrenhaus | 0.000000 | 0.000000 | 0.000000 | 0.001179 | 0.000000 | 0.000000 | 0.000 | 0.000780 | 0.000000 | 0.002295 |
| Mehrfamilienhaus | 0.123913 | 0.110276 | 0.186347 | 0.182783 | 0.226302 | 0.203767 | 0.375 | 0.251950 | 0.216769 | 0.233359 |
| Reihenendhaus | 0.063043 | 0.087719 | 0.099631 | 0.067217 | 0.057216 | 0.077055 | 0.000 | 0.072543 | 0.074642 | 0.068860 |
| Reihenmittelhaus | 0.104348 | 0.117794 | 0.147601 | 0.132075 | 0.123826 | 0.121575 | 0.250 | 0.100624 | 0.099182 | 0.074981 |
| Rustico | 0.002174 | 0.000000 | 0.000000 | 0.001179 | 0.000000 | 0.000000 | 0.000 | 0.000000 | 0.000000 | 0.000000 |
| Stadthaus | 0.019565 | 0.022556 | 0.020295 | 0.008255 | 0.013664 | 0.005993 | 0.000 | 0.010140 | 0.015337 | 0.006886 |
| Villa | 0.106522 | 0.037594 | 0.031365 | 0.044811 | 0.029889 | 0.020548 | 0.000 | 0.030421 | 0.016360 | 0.020658 |
| Wohn- und Geschäftshaus | 0.000000 | 0.007519 | 0.000000 | 0.002358 | 0.000854 | 0.000000 | 0.000 | 0.002340 | 0.000000 | 0.000765 |
Eine sehr gute Effizienzklasse, A oder A+, ist vor allem in Einfamilienhäusern und Doppelhaushälften zu finden. Eine sehr schlechte Effizienzklasse weisen jedoch ebenso Einfamilienhäuser auf, aber besonders Mehrfamilienhäuser und Doppelhaushälften.
Wie unterscheiden sich die numerischen Variablen je Stadt (nach Median)?
df.groupby(by="Ort").median().applymap('{:,.2f}'.format)
| MaxOrt | Preis | Flaeche | Raeume | Grundstuecksflaeche | Geschosse | Jahr | |
|---|---|---|---|---|---|---|---|
| Ort | |||||||
| berlin | 15.00 | 749,000.00 | 169.50 | 5.00 | 562.00 | 2.42 | 1,972.00 |
| frankfurt-am-main | 15.00 | 749,000.00 | 185.00 | 6.00 | 415.00 | 2.42 | 1,967.56 |
| hamburg | 15.00 | 695,000.00 | 161.50 | 5.00 | 550.50 | 2.42 | 1,969.00 |
| koeln | 15.00 | 619,000.00 | 167.00 | 6.00 | 432.00 | 2.42 | 1,968.00 |
| leipzig | 30.00 | 395,000.00 | 190.50 | 6.00 | 583.00 | 2.42 | 1,967.56 |
| muenchen | 15.00 | 1,295,000.00 | 200.00 | 6.00 | 387.23 | 2.42 | 1,981.00 |
| stuttgart | 20.00 | 750,000.00 | 199.00 | 8.00 | 368.00 | 2.42 | 1,967.56 |
Am teuersten sind mit Abstand Häuser in München, dieser Unterschied wird im folgenden Plot auch visualisiert.
Am weitesten vom Zentrum entfernt sind im Schnitt die Häuser in Leipzig und Stuttgart. In Stuttgart haben die Häuser auch die meisten Zimmer, im Durchschnitt acht. Die größte Wohnfläche und kleinste Grundstücksfläche haben Häuser in Stuttgart und München. Die Anzahl der Geschosse ist im Durchschnitt überall gleich, da es hier sehr viele Nullwerte gab und diese auf den Median gesetzt wurden. Die eher neuen Häuser, mit einem Median vom Jahr 1981, gibt es in München.
#Preis je Stadt
grouped = df.loc[:,['Ort', 'Preis']] \
.groupby(['Ort']) \
.median() \
.sort_values(by='Preis')
sns. set_theme(style="whitegrid")
sns.set(rc={'figure.figsize':(11,7)})
ax=sns.boxplot(x=df.Ort, y=df.Preis, order=grouped.index)
ax.set_xticklabels(ax.get_xticklabels(),rotation=90)
ax.set(ylim=(0, 3000000))
ax
<AxesSubplot:xlabel='Ort', ylabel='Preis'>
Der Median der Preise in Leipzig liegt sogar unterhalb der 25% günstigsten Häuser in allen anderen betrachteten Städte. Auch die Whisker sind vergleichweise kurz, so gibt es in Leipzig fast nur Ausreißer mit einem Preis höher als einer Million €. Auch in Köln und Stuttgart sind die Whisker und die Box eher näher beieinander. Die größte Preisspanne gibt es in München.
#Sind Häuser besserer Effizienzklasse eher teurer und in bestimmten Städten zu finden?
fig=px.scatter(df,x="Ort",y="Preis",color="Effizienz", range_y=(0, 2000000),
hover_data=["ID","Ort"],title="Zusammenhang Preis, Ort und Effizienz")
fig.show("notebook")
Im oben stehenden Plot können in der Legende die Effizienzklassen augewählt (aktiviert/desktiviert) werden. Leider ist keine Tendenz zu unserer Vermutung, dass Häuser mit guter Effizienzklasse eher mehr kosten, zu verzeichnen.
#Wie hängt die Effizienzklasse mit dem Energietraeger zusammen?
fig = px.density_heatmap(df, x="Energietraeger", y="Effizienz",
marginal_x="histogram", marginal_y="histogram")
fig.show("notebook")
Es gibt keine klare Korrelation zwischen Effizienzklasse und Energieträger, da mehrere Energieträger, wie Gas, Öl oder Fernwärme, in fast allen Effizienzklassen vertreten sind.
#Ändert sich die Art des Hauses je nach Entfernung zum Stadtzentrum?
fig = px.density_heatmap(df, x="Art", y="MaxOrt",
marginal_x="histogram", marginal_y="histogram")
fig.show("notebook")
Es gibt keine Villen, Bungalows und Bauernhäuser innerhalb 7 km zum Stadtzentrum. Zwischen 8 und 22 km vom Zentrum gibt es sehr viele Einfamilien-, Mehrfamilien- udn Reihenhäuser sowie Doppelhaushälften, all deren Anzahl nimmt mit größerem Abstand jedoch ab.
Im Regressionskapitel werden die vorhandenen Daten in ein verwendbares Format gebracht, sowie in ein Test- und Trainingsset unterteilt. Damit werden dann verschiedene Regressionsmodelle erstellt, optimiert und mit einander verglichen. Ziel ist es bestmöglich den Hauspreis vorherzusagen.
Im Preprocessing werden die Daten in ein verwendbares Format für die Regression gebracht. Das beinhaltet zum einen das One-Hot-Encoding kategorialer Features, zu anderen auch den split in ein Trainings- und Testset.
Beim One-Hot-Encoding wird jeder kategoriale Wert in eine eigene Spalte geschrieben und mit 1 = wahr oder 0 = falsch versehen. Damit werden alle kategorialen Features in Numerische umgewandelt und können damit besser verarbeitet werden in der Regression.
cat_features
['ID', 'Ort', 'Stand', 'Art', 'Effizienz', 'Energietraeger', 'Heizung']
df.shape
(8166, 14)
#One Hot Encoding kategorialer Features
dfOH=pd.get_dummies(df,columns=["Ort", "Art", "Effizienz", "Energietraeger", "Heizung"])
dfOH = dfOH.drop('ID', axis=1) #hilft nicht bei Regression
dfOH = dfOH.drop('Stand', axis=1) #hilft nicht bei Regression
pr=dfOH["Preis"] #ab hier: Preis als lettze Spalte setzen
dfOH.drop(labels=['Preis'], axis=1, inplace = True)
dfOH.insert(len(dfOH.columns), 'Preis', pr)
dfOH.head()
| MaxOrt | Flaeche | Raeume | Grundstuecksflaeche | Geschosse | Jahr | Ort_berlin | Ort_frankfurt-am-main | Ort_hamburg | Ort_koeln | ... | Heizung_Luft-/Wasser-Wärmepumpe, offener Kamin | Heizung_Luft-/Wasser-Wärmepumpe, offener Kamin, Zentralheizung | Heizung_Ofen | Heizung_Ofen, Zentralheizung | Heizung_Ofen, offener Kamin | Heizung_Ofen, offener Kamin, Zentralheizung | Heizung_Zentralheizung | Heizung_offener Kamin | Heizung_offener Kamin, Zentralheizung | Preis | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 15.0 | 780.0 | 30.0 | 1696.0 | 5.000000 | 1890.0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 7790000 |
| 1 | 30.0 | 111.0 | 5.0 | 1100.0 | 2.000000 | 1936.0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 450000 |
| 2 | 10.0 | 323.0 | 8.0 | 393.0 | 2.417838 | 1990.0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 690000 |
| 3 | 10.0 | 97.0 | 4.0 | 407.0 | 2.417838 | 1958.0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 429000 |
| 4 | 10.0 | 18662.0 | 8.0 | 502.0 | 2.417838 | 1936.0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1700000 |
5 rows × 132 columns
Das Dataframe wird nun in zwei Teile gesplittet: Test- und Trainingsdaten. Mit den Trainingsdaten wird das Modell trainiert und es lernt die Testdaten nicht kennen. Die Vorhersagen des Modells aus den Trainingsdaten werden dann mit den Trainingsdaten abgegelichen, um zu schauen wie gut die Performance tatsächlich mit neuen Daten ist. Innerhalb dieser Sets wird wiederum in X und y unterschieden: X sind die Features, durch die das Modell lernt und y ist der Wert, der vorhergesagt werden soll (hier: Preis).
Die Standardisierung der numerischen Features dient dazu alle numerischen Features auf die selbe Skalierung zu bringen, sodass große Zahlen keinen größeren Einfluss auf das Modell haben, sondern deren Verhältnis ist das Wichtige.
#Split X y
X=dfOH.iloc[:,:-1]
y=dfOH.iloc[:,-1]
#Standardisierung von X
scaler = MinMaxScaler() #getestet: StandardScaler, MinMaxScaler (ein Einfluss auf R2)
X=pd.DataFrame(scaler.fit_transform(X), columns=X.columns)
#Split Test Train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,random_state=0)
print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)
(5716, 131) (2450, 131) (5716,) (2450,)
Durch den Split und das Preprocessing haben wir nun unser Trainingsset mit 5716 Einträgen, also 70%, und das Testset mit den anderen 30%, also 2450 Zeilen. Die Anzahl der Spalten von X hat sich durch das One-Hot-Encoding enorm erhöht, von 12 auf 131.
Mit den enstandenen Datensets werden nun verschiedene Regressionsmodelle erstellt, um den Hauspreis vorherzusagen. Dafür wird erst die Library statsmodel für Ordinary Least Squares (OLS = Lineare Regression) verwendet und danach sklearn für Pipelines, wo mit Crossvalidierung mehrere Modelle verglichen und optimiert werden.
Zuerst wird ein OLS Modell von Preis zu einem Feature erstellt. Hierbei wird Flaeche gewählt, da es die höchste Korrelation aufweist.
#statsmodel mit einem Feature
lm = smf.ols(formula ='Preis ~ Flaeche', data=dfOH).fit()
#Regression results
lm.summary()
| Dep. Variable: | Preis | R-squared: | 0.038 |
|---|---|---|---|
| Model: | OLS | Adj. R-squared: | 0.038 |
| Method: | Least Squares | F-statistic: | 319.4 |
| Date: | Fri, 06 Jan 2023 | Prob (F-statistic): | 4.23e-70 |
| Time: | 13:01:11 | Log-Likelihood: | -1.2700e+05 |
| No. Observations: | 8166 | AIC: | 2.540e+05 |
| Df Residuals: | 8164 | BIC: | 2.540e+05 |
| Df Model: | 1 | ||
| Covariance Type: | nonrobust |
| coef | std err | t | P>|t| | [0.025 | 0.975] | |
|---|---|---|---|---|---|---|
| Intercept | 9.682e+05 | 1.59e+04 | 60.710 | 0.000 | 9.37e+05 | 9.99e+05 |
| Flaeche | 25.3055 | 1.416 | 17.871 | 0.000 | 22.530 | 28.081 |
| Omnibus: | 11010.814 | Durbin-Watson: | 1.517 |
|---|---|---|---|
| Prob(Omnibus): | 0.000 | Jarque-Bera (JB): | 3139703.091 |
| Skew: | 7.653 | Prob(JB): | 0.00 |
| Kurtosis: | 97.833 | Cond. No. | 1.18e+04 |
Ergebnisse OLS mit einem Feature:
Nun werden Ausreißer betrachtet, denn in der EDA wurden bereits einige Ausreißer identifiziert, aber nicht beseitigt. Diese können nun die Modellperformance beinflusssen. Als Methode wird Cook's Distance verwendet.
#Cook's distance
lm_cooksd = lm.get_influence().cooks_distance[0]
#Länge von dfOH für n
n = len(dfOH['Preis'])
#Critical d
critical_d = 4/n
print('Critical Cooks distance:', critical_d)
#Identifikation möglicher Ausreißer mit Leverage
out_d = lm_cooksd > critical_d
#Ausreißer entfernen
oultiers = dfOH.index[out_d]
subset = ~dfOH.index.isin(['outliers'])
Critical Cooks distance: 0.0004898359049718344
Nun wird das OLS mit einem Feature, Flaeche, erneut durchgeführt, jedoch werden die Ausreißer über das subset entfernt.
#statsmodel mit einem Feature und ohne Ausreißer
lm2 = ols("Preis ~ Flaeche", data=dfOH, subset=subset).fit()
print(lm2.summary())
OLS Regression Results
==============================================================================
Dep. Variable: Preis R-squared: 0.038
Model: OLS Adj. R-squared: 0.038
Method: Least Squares F-statistic: 319.4
Date: Fri, 06 Jan 2023 Prob (F-statistic): 4.23e-70
Time: 13:01:17 Log-Likelihood: -1.2700e+05
No. Observations: 8166 AIC: 2.540e+05
Df Residuals: 8164 BIC: 2.540e+05
Df Model: 1
Covariance Type: nonrobust
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
Intercept 9.682e+05 1.59e+04 60.710 0.000 9.37e+05 9.99e+05
Flaeche 25.3055 1.416 17.871 0.000 22.530 28.081
==============================================================================
Omnibus: 11010.814 Durbin-Watson: 1.517
Prob(Omnibus): 0.000 Jarque-Bera (JB): 3139703.091
Skew: 7.653 Prob(JB): 0.00
Kurtosis: 97.833 Cond. No. 1.18e+04
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.18e+04. This might indicate that there are
strong multicollinearity or other numerical problems.
Ergebnisse OLS mit einem Feature und ohne Ausreißer:
Keine Performanceverbesserung bzw. Veränderung zu mit Ausreißern zu verzeichnen.
Für ein OLS Modell mit mehreren Features, wird zuvor die Wichtigkeit der Features untersucht. Je größer der Balken im nachfolgenden Plot, desto größer die Wichtigkeit. Aus den wichtigsten Features wird dann ein neues OLS Modell gebaut.
#Feature importance
reg = LassoCV(cv=5, random_state=10, max_iter=10000).fit(X_train, y_train)
#Absolute Werte der Koeffizienten
importance = np.abs(reg.coef_)
feature_names = X_train.columns
sns.set(font_scale = 0.4) #Plot mit Zoom-Funktion sichten oder Versuch Features = 0 nicht anzeigen
sns.barplot(x=importance,
y=feature_names)
<AxesSubplot:>
Da sich der Feature Importance Plot kaum lesen lässt, wird auf eine alternative Feature Selection Mthode, Forward Selection zurückgegriffen.
tic_fwd = time()
sfs_forward = SequentialFeatureSelector(
reg, n_features_to_select=5,
direction="forward").fit(X_train, y_train)
toc_fwd = time()
print(
"Features selected by forward sequential selection: "
f"{feature_names[sfs_forward.get_support()]}"
)
print(f"Done in {toc_fwd - tic_fwd:.3f}s")
Features selected by forward sequential selection: Index(['MaxOrt', 'Flaeche', 'Ort_muenchen', 'Art_Mehrfamilienhaus',
'Art_Villa'],
dtype='object')
Done in 84.240s
lm3 = ols("Preis ~ MaxOrt + Flaeche + Ort_muenchen + Art_Mehrfamilienhaus + Art_Villa", data=dfOH).fit()
print(lm3.summary())
OLS Regression Results
==============================================================================
Dep. Variable: Preis R-squared: 0.191
Model: OLS Adj. R-squared: 0.191
Method: Least Squares F-statistic: 385.6
Date: Fri, 06 Jan 2023 Prob (F-statistic): 0.00
Time: 13:02:48 Log-Likelihood: -1.2629e+05
No. Observations: 8166 AIC: 2.526e+05
Df Residuals: 8160 BIC: 2.526e+05
Df Model: 5
Covariance Type: nonrobust
========================================================================================
coef std err t P>|t| [0.025 0.975]
----------------------------------------------------------------------------------------
Intercept 1.121e+06 2.84e+04 39.411 0.000 1.06e+06 1.18e+06
MaxOrt -2.124e+04 1072.608 -19.802 0.000 -2.33e+04 -1.91e+04
Flaeche 19.3178 1.314 14.707 0.000 16.743 21.893
Ort_muenchen 9.922e+05 4.02e+04 24.681 0.000 9.13e+05 1.07e+06
Art_Mehrfamilienhaus 5.344e+05 3.49e+04 15.313 0.000 4.66e+05 6.03e+05
Art_Villa 1.599e+06 7.98e+04 20.029 0.000 1.44e+06 1.76e+06
==============================================================================
Omnibus: 11287.361 Durbin-Watson: 1.691
Prob(Omnibus): 0.000 Jarque-Bera (JB): 4109440.829
Skew: 7.926 Prob(JB): 0.00
Kurtosis: 111.749 Cond. No. 6.47e+04
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 6.47e+04. This might indicate that there are
strong multicollinearity or other numerical problems.
Ergebnisse OLS mit den wichtigsten Features:
Nun wird überprüft, ob Multikollinearität zwischen den verwendeten Features vorliegt. Multikollinearität beschreibt die Korrelation von Features untereinander, wenn der Variance Inflation Factor (VIF) > 5. Diese muss entfernt werden, da das Modell sonst zu Overfitting tendiert.
#Multicollinearity
y, X = dmatrices('Preis ~ MaxOrt + Flaeche + Ort_muenchen + Art_Mehrfamilienhaus + Art_Villa', dfOH, return_type='dataframe')
#Für jedes X wird der VIF berechnet und in ein df gespeichert
vif = pd.DataFrame()
vif["VIF Factor"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
vif["Feature"] = X.columns
vif.round(2)
| VIF Factor | Feature | |
|---|---|---|
| 0 | 4.16 | Intercept |
| 1 | 1.01 | MaxOrt |
| 2 | 1.02 | Flaeche |
| 3 | 1.01 | Ort_muenchen |
| 4 | 1.03 | Art_Mehrfamilienhaus |
| 5 | 1.01 | Art_Villa |
Der VIF zwischen den Features liegt immer unterhalb von 5, daher liegt keine kritische Multikollinearität im Modell vor.
Das OLS Modell mit den wichtigsten Features hat bei statsmodel die beste Performance erzielt, mit einem R2 von 0,19.
Mit sklearn werden weitere Regressionsmodelle verwendet, optimiert und verglichen. Dafür werden alle Parameter optimeriert. Die betrachteten Modelle sind:
#Pipelines für mehrere Methoden der Regression
#Linear Regression
pipe1 = Pipeline([('scaler', StandardScaler()),
('lr', LinearRegression(fit_intercept=True, #Nicht zentrierte Daten, daher intercept=Ture benötigt
copy_X=True, #getestet: True, False (komisches Ergebnis bei False)
n_jobs=None, #nicht benötigt, da Problem nicht so groß
positive=False))]) #True nur bei dense arryas möglich, hier nicht
#Bayesian Ridge
pipe2 = Pipeline([('scaler', StandardScaler()),
('br', BayesianRidge(n_iter=300, #getestet: 300, 50, 500 (kein Einfluss auf R2)
tol=0.001, #getestet: 0.001, 0.1, 0.0001 (0.1 komisches Ergebnis, andere Werte keinen Einfluss)
alpha_1=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (kein Eifnluss auf R2)
alpha_2=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (kein Einfluss auf R2)
lambda_1=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (winziges Ergebnis bei 1e-02, kein Einfluss anderer Werte auf R2)
lambda_2=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (kein Einfluss auf R2)
alpha_init=None, #getestet: None, 0.1, 1 (kein Einfluss auf R2)
lambda_init=None, #getestet: None, 0.1, 2 (kein Einfluss auf R2)
compute_score=False, #getestes: False, True (kein Einfluss auf R2)
fit_intercept=True, #Nicht zentrierte Daten, daher intercept=Ture benötigt
copy_X=True, #getestet: True, False (kein Einfluss auf R2)
verbose=False))]) #getestet: False, True (kein Einfluss auf R2)
#Support Vector Regression
#keinen positiven R2 erreicht
pipe3 = Pipeline([('scaler', StandardScaler()),
('svr', SVR(kernel='rbf', #getestet: linear, poly, rbf, sigmoid, precomputed, callable (negatives Ergebnis bei rbf, linear, poly und sigmoid, Error bei precomputed und callable)
degree=10, #nur für poly, getestet: 3, 5, 10 (bestes Ergebnis, aber mit Meldung für preprocessing, owbohl diese gemacht wird)
gamma='scale', #getestet: scale, auto, 1.0 (kein Einfluss auf R2)
coef0=0.0, #nur für poly und sigmoig, getestet: 0.0, 1.0
tol=0.0001, #getestet: 0.001, 0.1, 0.0001 (kein Einfluss auf R2)
C=1.0, #getestet: 1.0, 0.1 (kein Einfluss auf R2)
epsilon=0.1, #getestet: 0.1, 1.0 (kein Einfluss auf R2)
shrinking=True, #getestet: True, False (kein Einfluss auf R2)
cache_size=200, #keep 100
verbose=False, #getestet: False, True (kein Einfluss auf R2)
max_iter=10000))]) #getestet -1, 10, 1000, 10000 (nur bei 10000 und -1 keine Meldung, dass max_iter erhöht werden soll)
#Stochastic Gradient Descent Regressor
#keinen positiven R2 erreicht
pipe4 = Pipeline([('scaler', StandardScaler()),
('sgd', SGDRegressor(loss='huber', #getestet: squared_error, huber, epsilon_insensitive, squared_epsilon_insensitive (negatives Ergebnis bei squared_error, huber, epsilon_insensitive, squared_epsilon_insensitive)
penalty='elasticnet', #getestet: l2, l1, elasticnet, None (kein Einfluss auf R2)
alpha=0.1, #getestet: 0.0001, 0.1 (kein Einfluss auf R2)
l1_ratio=0.15, #nur für elasticnet, getestet: 0.15, 0.5, 0.75 (kein Einfluss auf R2)
fit_intercept=True, #Nicht zentrierte Daten, daher intercept=Ture benötigt
max_iter=10000, #getestet: 1000, 10000
tol=0.001, #getestet: 0.001, 0.1, None (None verlängert Laufzeit enorm, bei anderen Werten kein Eifnluss auf R2)
shuffle=True, #getestet: True, False (kein Einfluss auf R2)
verbose=0, #getestet: 0, 1, 10 (kein Einfluss auf R2)
epsilon=1.0, #getestet: 0.1, 1.0 (kein Einfluss auf R2)
random_state=None, #keep None
learning_rate='adaptive', #getestet: invscaling, constant, optimal, adaptive (kein Einfluss auf R2)
eta0=0.01, #getestet: 0.01, 0.1 (kein Einfluss auf R2)
power_t=0.25, #nur für invscaling, getestet: 0.25, 1.0 (kein Einfluss auf R2)
early_stopping=True, #getestet: False, True (True verkürzt Laufzeit enorm mit nur minimal schlechterem Ergebnis)
validation_fraction=0.1, #nur für earla_stopping=True, getestet: 0.1, 0.5 (kein Einfluss auf R2)
n_iter_no_change=5, #getestet: 5, 50 (kein Einfluss auf R2)
warm_start=False, #getestet: False, True (kein Einfluss auf R2)
average=False))]) #getestet: False, True
#Decision Tree Regressor
pipe5 = Pipeline([('scaler', StandardScaler()),
('dtr', DecisionTreeRegressor(criterion='poisson', #getestet: squared_error, friedman_mse, absolute_error, poisson
splitter='best', #getestet: best, random
max_depth=5, #getestet: None, 5, 10 (nur mit 5 positives Ergebnis bei allen Varianten für R2)
min_samples_split=2, #getestet: 2, 5, 10 (kein Einfluss auf R2)
min_samples_leaf=10, #getestet: 1, 3, 10
min_weight_fraction_leaf=0.0, #getestet: 0.0, 0.1, 0.3
max_features=None, #getestet: None, 5, 10
random_state=None, #keep None
max_leaf_nodes=None, #getestet: None, 5, 10
min_impurity_decrease=1.0, #getestet: 0.0, 0.3, 1.0 (kein Einfluss auf R2)
ccp_alpha=0.0))]) #getestet: 0.0, 0.3, 1.0 (kein Einfluss auf R2)
Für die Modelle wird nun eine 10-Fold Crossvalisierung durchgeführt und die durchschnittlichen R2-Werte danach ausgegeben.
# Cross validation jeder Pipeline
scores = [cross_val_score(mypipe, X_train, y_train, scoring='r2', cv=10)
for mypipe in [pipe1,pipe2,pipe3,pipe4,pipe5]]
#Ausgabe der Ergebnisse
for score,label in zip(scores,
['Linear Regression',
'Bayesian Ridge',
'Support Vector Regression',
'Stochastic Gradient Descent Regressor',
'Decision Tree Regressor'
]
):
print("R2: {:.2} (+/- {:.2}), {:}".format(score.mean(), score.std(), label))
R2: -1.5e+23 (+/- 1.3e+23), Linear Regression R2: 0.24 (+/- 0.054), Bayesian Ridge R2: -0.064 (+/- 0.016), Support Vector Regression R2: -0.68 (+/- 0.2), Stochastic Gradient Descent Regressor R2: 0.29 (+/- 0.088), Decision Tree Regressor
Ergebnisse:
Der R2 Wert ist das Bestimmtheitsmaß und sollte zwischen 0 und 1 liegen. Es ist komisch, dass hier negative Werte herauskommen. Die einzigen realistischen Ergebnisse sind bei Bayesian Ridge und dem Decision Tree Regressor. Der Decision Tree hat mit einem R2 von 0.29 die beste PErformance.
Da wir den Fehler in der Crossvalidierung nicht finden konnten, berechnen wir nun die Werte manuell mit einzelner Crossvalidierung.
#Vergleich Vorhersage zu tatsächlichem Hauspreis
pipe1.fit(X_train, y_train)
pipe2.fit(X_train, y_train)
pipe3.fit(X_train, y_train)
pipe4.fit(X_train, y_train)
pipe5.fit(X_train, y_train)
Pipeline(steps=[('scaler', StandardScaler()),
('dtr',
DecisionTreeRegressor(criterion='poisson', max_depth=5,
min_impurity_decrease=1.0,
min_samples_leaf=10))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. Pipeline(steps=[('scaler', StandardScaler()),
('dtr',
DecisionTreeRegressor(criterion='poisson', max_depth=5,
min_impurity_decrease=1.0,
min_samples_leaf=10))])StandardScaler()
DecisionTreeRegressor(criterion='poisson', max_depth=5,
min_impurity_decrease=1.0, min_samples_leaf=10)#cross validation score
scores_1 = cross_val_score(pipe1, X_train, y_train, scoring = 'r2', cv = 10)
scores_2 = cross_val_score(pipe2, X_train, y_train, scoring = 'r2', cv = 10)
scores_3 = cross_val_score(pipe3, X_train, y_train, scoring = 'r2', cv = 10)
scores_4 = cross_val_score(pipe4, X_train, y_train, scoring = 'r2', cv = 10)
scores_5 = cross_val_score(pipe5, X_train, y_train, scoring = 'r2', cv = 10)
print("mean cross validation score Linear Regression: {:.2}".format(np.mean(scores_1)))
print("mean cross validation score Bayesian Ridge: {:.2}".format(np.mean(scores_2)))
print("mean cross validation score Support Vector Machine: {:.2}".format(np.mean(scores_3)))
print("mean cross validation score Stochastic Gradient Descent Regressor: {:.2}".format(np.mean(scores_4)))
print("mean cross validation score Decision Tree Regressor: {:.2}".format(np.mean(scores_5)))
mean cross validation score Linear Regression: -1.5e+23 mean cross validation score Bayesian Ridge: 0.24 mean cross validation score Support Vector Machine: -0.064 mean cross validation score Stochastic Gradient Descent Regressor: -0.68 mean cross validation score Decision Tree Regressor: 0.29
Die manuelle Crossvalidierung ergibt die selben Ergebnisse. Nun wird noch versucht mit y_pred und r2_score() ein zuverlässigeren Ergebnis zu erzielen.
#Werte, die das Modell vorhersagt
y_pred_1 = pipe1.predict(X_train)
y_pred_2 = pipe2.predict(X_train)
y_pred_3 = pipe3.predict(X_train)
y_pred_4 = pipe4.predict(X_train)
y_pred_5 = pipe5.predict(X_train)
#R2 aus tatsächlichen vs. vorhergesagten Werten
R2_1 = r2_score(y_train, y_pred_1)
R2_2 = r2_score(y_train, y_pred_2)
R2_3 = r2_score(y_train, y_pred_3)
R2_4 = r2_score(y_train, y_pred_4)
R2_5 = r2_score(y_train, y_pred_5)
print("R2 Linear Regression:", R2_1.round(2))
print("R2 Bayesian Ridge:", R2_2.round(2))
print("R2 Support Vector Machine:", R2_3.round(2))
print("R2 Stochastic Gradient Descent Regressor:", R2_4.round(2))
print("R2 Decision Tree Regressor:", R2_5.round(2))
R2 Linear Regression: 0.26 R2 Bayesian Ridge: 0.26 R2 Support Vector Machine: -0.06 R2 Stochastic Gradient Descent Regressor: -0.6 R2 Decision Tree Regressor: 0.43
Ergebnisse:
Hiermit sind die unterschiede etwas anders und nur noch 2 der 5 Modelle zeigen einen negativen R2-Wert. Den besten R2 hat mit 0.43 auch hier der Decision Tree Regressor. Dies ist also das Modell, welches den Hauspreis am besten vorhersagt, auch besser als die Lineare Regression mit statsmodel.
Es wird untersucht, wie lange die Inserate im Durchschnitt online sind. Dies gibt einen Aufschluss darüber, wie lange es dauert bis Häuser verkauft bzw. ausreichend Interessenten für den Kauf vorhanden sind.
#Zählen wie häufig die IDs vorkommen, jedes Vorkommen ist ein Tag
inv = immodf.groupby(['ID']).size().reset_index(name='count')
inv = inv[~inv['count'].isin([2])]
inv.head()
| ID | count | |
|---|---|---|
| 0 | 222f85z | 8 |
| 1 | 222mb5l | 8 |
| 2 | 223j85z | 8 |
| 3 | 223m85z | 8 |
| 4 | 224g85z | 8 |
#Ausprägung von count
inv.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| count | 7656.0 | 7.108934 | 1.792325 | 1.0 | 7.0 | 8.0 | 8.0 | 11.0 |
Die kürzeste Inventardauer beträgt einen Tag und die höchste 8 Tage. Im Durchschnitt sind die Häuser 7 Tage lang inseriert.
Bei einer längeren Betrachtungsdauer würde sich ein zuverlässigerer Wert der Inventardauer herauskristallisieren. Da auch immer nur 50 Seiten statt aller betrachtet werden, kann es sein, dass eineige Häuser auch nur auf Seite 51 oder folgende gerutscht sind, aber eigentlich noch inseriert sind.
Unterscheidet sich die Inventardauer je Stadt?
#Durchschnittliche Inventardauer pro Ort
inv = immodf.groupby(['ID','Ort']).size().reset_index(name='count')
inv = inv[~inv['count'].isin([20])]
inv.groupby(by="Ort").describe().applymap('{:,.2f}'.format)
| count | ||||||||
|---|---|---|---|---|---|---|---|---|
| count | mean | std | min | 25% | 50% | 75% | max | |
| Ort | ||||||||
| berlin | 1,189.00 | 6.73 | 2.23 | 1.00 | 6.00 | 8.00 | 8.00 | 11.00 |
| frankfurt-am-main | 1,143.00 | 6.98 | 2.04 | 1.00 | 7.00 | 8.00 | 8.00 | 9.00 |
| hamburg | 1,170.00 | 6.84 | 2.19 | 1.00 | 7.00 | 8.00 | 8.00 | 8.00 |
| koeln | 1,133.00 | 7.06 | 1.97 | 1.00 | 8.00 | 8.00 | 8.00 | 9.00 |
| leipzig | 1,117.00 | 7.16 | 1.93 | 1.00 | 8.00 | 8.00 | 8.00 | 9.00 |
| muenchen | 1,096.00 | 7.30 | 1.80 | 1.00 | 8.00 | 8.00 | 8.00 | 8.00 |
| stuttgart | 1,115.00 | 6.33 | 1.70 | 1.00 | 7.00 | 7.00 | 7.00 | 8.00 |
Die durchschnittlich längste Inventardauer haben Exposes in Leipzig, am kürzesten in Stuttgart. Die Werte liegen jedoch alle sehr nah bei einander, bei 6,33 bis 7,3 Tagen.
In diesem letzten Kapitel werden die täglich eingelesenen Bauzinsen mit den Hauspreisen in Verbidnung gebracht. Dabei wird untersucht, ob die Zinshöhe den Hauspreis beeinflusst.
Zuerst werden die täglich gescrapten Zinsen eingelesen und danach in ein gemeinsames dataframe gemerget. Das Vorgehen ist dasselbe wie beim Einlesen der Immoboliendateien.
#Upload
zins2812=pd.read_csv("daily_data//zins//2022-12-28_Zinsen.csv")
zins2912=pd.read_csv("daily_data//zins//2022-12-29_Zinsen.csv")
zins3012=pd.read_csv("daily_data//zins//2022-12-30_Zinsen.csv")
zins3112=pd.read_csv("daily_data//zins//2022-12-31_Zinsen.csv")
zins0101=pd.read_csv("daily_data//zins//2023-01-01_Zinsen.csv")
zins0201=pd.read_csv("daily_data//zins//2023-01-02_Zinsen.csv")
zins0301=pd.read_csv("daily_data//zins//2023-01-03_Zinsen.csv")
zins0401=pd.read_csv("daily_data//zins//2023-01-04_Zinsen.csv")
zins0501=pd.read_csv("daily_data//zins//2023-01-05_Zinsen.csv")
zins2912
| Unnamed: 0 | Sollzinsbindung | Effektiver Jahreszins | Datenstand | |
|---|---|---|---|---|
| 0 | 0 | 5 Jahre | ['3,53'] | ['28.12.2022 16:30'] |
| 1 | 1 | 10 Jahre | ['3,51'] | ['28.12.2022 16:30'] |
| 2 | 2 | 15 Jahre | ['3,64'] | ['28.12.2022 16:30'] |
| 3 | 3 | 20 Jahre | ['3,79'] | ['28.12.2022 16:30'] |
| 4 | 4 | 25 Jahre | ['3,86'] | ['28.12.2022 16:30'] |
#Merge
zinsdf = pd.concat([zins2812,zins2912,zins3012,zins3112,zins0101,zins0201,zins0301,zins0401,zins0501],ignore_index=True)
print(zinsdf.shape)
zinsdf.head(10)
(45, 4)
| Unnamed: 0 | Sollzinsbindung | Effektiver Jahreszins | Datenstand | |
|---|---|---|---|---|
| 0 | 0 | 5 Jahre | ['3,53'] | ['28.12.2022 11:30'] |
| 1 | 1 | 10 Jahre | ['3,51'] | ['28.12.2022 11:30'] |
| 2 | 2 | 15 Jahre | ['3,64'] | ['28.12.2022 11:30'] |
| 3 | 3 | 20 Jahre | ['3,79'] | ['28.12.2022 11:30'] |
| 4 | 4 | 25 Jahre | ['3,86'] | ['28.12.2022 11:30'] |
| 5 | 0 | 5 Jahre | ['3,53'] | ['28.12.2022 16:30'] |
| 6 | 1 | 10 Jahre | ['3,51'] | ['28.12.2022 16:30'] |
| 7 | 2 | 15 Jahre | ['3,64'] | ['28.12.2022 16:30'] |
| 8 | 3 | 20 Jahre | ['3,79'] | ['28.12.2022 16:30'] |
| 9 | 4 | 25 Jahre | ['3,86'] | ['28.12.2022 16:30'] |
Auch im zinsdf sind noch einige ungewollte Sonderzeichen und Worte inbegriffen. Diese werden im nächsten Schritt entfernt.
#data prep
zinsdf['Sollzinsbindung'] = zinsdf['Sollzinsbindung'].str.replace(" Jahre","")
zinsdf.loc[:,"Sollzinsbindung"]=pd.to_numeric(zinsdf["Sollzinsbindung"])
zinsdf['Jahreszins'] = zinsdf['Effektiver Jahreszins'].apply(lambda x: x[1:-1])
zinsdf['Jahreszins'] = zinsdf['Jahreszins'].str.replace(",",".")
zinsdf['Jahreszins'] = zinsdf['Jahreszins'].str.replace("'","")
zinsdf.loc[:,"Jahreszins"]=pd.to_numeric(zinsdf["Jahreszins"])
zinsdf = zinsdf.drop('Effektiver Jahreszins', axis=1)
zinsdf['Stand'] = zinsdf['Datenstand'].apply(lambda x: x[1:-1])
zinsdf['Stand'] = zinsdf['Stand'].str.replace(",",".")
zinsdf['Stand'] = zinsdf['Stand'].str.replace("'","")
zinsdf['Stand'] = zinsdf['Stand'].str.replace("28.12.2022 16:30","29.12.2022 16:30")
#zinsdf['Stand'] = pd.to_datetime(zinsdf['Stand'], format='%d%m%Y:%H:%M:%S') #Zeitformat stimmt noch nicht
zinsdf = zinsdf.drop('Datenstand', axis=1)
zinsdf = zinsdf.drop('Unnamed: 0', axis=1)
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/1162883209.py:14: FutureWarning: The default value of regex will change from True to False in a future version.
zinsdf.head(10)
| Sollzinsbindung | Jahreszins | Stand | |
|---|---|---|---|
| 0 | 5 | 3.53 | 28.12.2022 11:30 |
| 1 | 10 | 3.51 | 28.12.2022 11:30 |
| 2 | 15 | 3.64 | 28.12.2022 11:30 |
| 3 | 20 | 3.79 | 28.12.2022 11:30 |
| 4 | 25 | 3.86 | 28.12.2022 11:30 |
| 5 | 5 | 3.53 | 29.12.2022 16:30 |
| 6 | 10 | 3.51 | 29.12.2022 16:30 |
| 7 | 15 | 3.64 | 29.12.2022 16:30 |
| 8 | 20 | 3.79 | 29.12.2022 16:30 |
| 9 | 25 | 3.86 | 29.12.2022 16:30 |
Das zinsdf ist nun zur weiteren Verwendung bereit.
Es wird untersucht, ob die Zinshöhe die Hauspreise beeinflusst. Die Vermutung ist, dass höhere Bauzinsen etwas niedrigere Hauspreise bedeuten.
Zuerst werden die Bauzinsen und Hauspreise einzeln visuell dargestellt und danach in kombiniert.
#Zinshöhe
sns.pointplot(data=zinsdf, x='Stand', y='Jahreszins', hue='Sollzinsbindung')
<AxesSubplot:xlabel='Stand', ylabel='Jahreszins'>
Die Zinshöhe bei einer Sollzinsbindung von 5 und 25 Jahren stagniert ziemlich. Die Bauzinsen bei einer Sollzinsbindung von 20 Jahren fallen leicht, hingegen bei einer Sollzinsbindung von 10 und 15 Jahren steigen sie im betrachteten Zeitraum zuerst und fallen am letzten Tag enorm.
#Hauspreise
sns.pointplot(data=df, x='Stand', y='Preis')
<AxesSubplot:xlabel='Stand', ylabel='Preis'>
Die Hauspreise der neu inserierten Häuser fallen vom 28. auf 29.12.2022, steigen danach jedoch wieder auf den ursprünglichen Durchschnittswert.
#Bauzinsen und Hauspreise kombiniert
fig,(ax1, ax2)= plt.subplots(nrows=2)
fig.set_size_inches(18, 14)
sns.pointplot(data=zinsdf, x='Stand', y='Jahreszins', hue='Sollzinsbindung', ax=ax1)
sns.pointplot(data=df, x='Stand', y='Preis', ax=ax2)
<AxesSubplot:xlabel='Stand', ylabel='Preis'>
Im direkten Vergleich lässt sich keine ähnliche oder entgegensetzte Struktur der Verläufe von Hauspreisen und Zinsen ablesen. Der betrachtete Zeitraum ist jedoch mit nur wenigen Tagen sehr kurz. Bei einer Fortführung über längeren Zeitraum ist es möglich, dass einen Zusammenhang ablesbar werden könnte. Dann könnte außerdem mit einem neuronalen Netz eine Zeitreihenvorhersage für Hauspreise und Bauzinssätze durchgeführt werden.
Für das Projekt wurden zuerst die inserierten Häuser von Immowelt aus sieben deutschen Städten gescrapet. Bis zum letztendlich verwendeten Code wurden einige Schritte mit xpath durchgeführt: Zuerst wurde eine Seite eines Ortes gescrapet, dann alle Seiten eines Ortes, außerdem wurde in alle Exposes gegangen um weitere Eigenschaften der Häuser zu inkludieren, und zuletzt wurde das für alle Orte durchgeführt. Am Ende sind alle Schritte in einer großen For-Schleife vereint und werden von einer Matrix in ein Pandas Dataframe umgewandelt. Dabei war es von Vorteil, dass die URL sehr logisch aufgebaut ist und sich einzelne Bausteine ändern lassen, um so auf eine andere Seite und anderen Ort zu wechseln. Das selbe gilt für die URL der Exposes. Kompliziert wurde Webscraping jedoch durch die For-Schleifen, Informationen, die nicht immer an derselben Stelle stehen und Teile, wie die Seitenzahl, die sich nicht scrapen ließen.
Das Webscraping von Immowelt sollte 1x täglich auf dem aws Server EC2 per Putty automatisch laufen. Dieser Vorgang wurde jedoch häufig unterbrochen, weshalb wir das Update manuell auf dem eigenen Rechner gestartet haben.
Außerdem wurden die Bauzinsen von Comdirect gescrapet. Dabei wurde je nach Sollzinsbindung unterschieden. Auch hier wurde per xpath gescrapt und es gab keine Komplikationen.
Das tägliche Scraping von Comdirect llief auch problemlos auf dem aws Server und gab je eine csv-Datei aus.
Die Daten der Häuser von Immowelt wurden dann in diesem separaten Notebook eingelesen und analysiert. Zu Beginn waren es 55060 Zeilen und 16 Spalten, welche von Sonderzeichen, sowie unnötigen Buchstaben und Spalten befreit wurden und daraufhin in den korrekten Datentyp umgewandelt wurden. Danach wurden Nullwerte ersetzt und für die Regression Duplikate entfernt. Dieses gekürzte Dataframe wurde dann explorativ analysiert, zuerst einzelne Features und darauffolgend Zusammenhänge zwischen diesen. Erstaunlich war, dass es keine starke Korrelation einzelner Features zum Preis gibt. Außerdem sind bei fast allen Variablen viele Ausreißer vorhanden. Eine weitere interessante Erkenntnis war, dass Häuser in Leipzig besonders günstig und in München besonders teuer sind, in allen anderen Städten sich die Preise jedoch in einem ähnlichen Rahmen bewegen.
Nach diesen Erkenntnissen, wurden die Daten für die Regression vorbereitet. Die kategorialen Fetures wurden One-Hot-Encoded und die numerischen Features standardisiert. Das Dataframe außerdem in X und y (zu bestimmender Preis), sowie Trainings- und Testdaten unterteilt. Mit diesen Daten wurden mehrere Regressionsmodelle getestet, optimiert und verglichen. Bei den Modellen handelt es sch um lineare Regression mit statsmodel, sowie Lineare Regression, Bayesian Ridge, Support Vector, Stochastic Gradient Descent und Decision Trees mit sklearn. Die Modellperformance aller Modelle ist leider nicht besonders gut, also der Hauspreis wird nicht zuverlässig vorhergesagt. Außerdem sind manche R2 Werte negativ, obwohl diese zwischen 0 und 1 liegen sollten. Am besten war der Decision Tree mit R2 von 0,43.
Anhand des großden Dataframes mit Duplikaten wurde die Inventardauer der Exposes bestimmt. Es wurde gezählt wie häufig diese Vorkommen und beim Betrachtungszeitraum von 8 Tagen, waren die Häuser durchschnittlich über 7 Tage inseriert.
Zuletzt wurden die Hauspreise mit den Bauzinsen angereichert. Die aufgestellt Hypothese war, dass die Hauspreise mit steigenden Zinsen fallen und vice versa. Im Betrachtungszeitraum hat sich diese Hypothese jedoch nciht bewahrheitet, stattdessen konnte kein Zusammenhang zwischen Hauspreisen und Bauzinsen festgestellt werden.